const later = (function() {
    'use strict';
    var later = {
        version: '1.2.0'
    };
    if (!Array.prototype.indexOf) {
        Array.prototype.indexOf = function(searchElement) {
            'use strict';
            if (this == null) {
                throw new TypeError();
            }
            var t = Object(this);
            var len = t.length >>> 0;
            if (len === 0) {
                return -1;
            }
            var n = 0;
            if (arguments.length > 1) {
                n = Number(arguments[1]);
                if (n != n) {
                    n = 0;
                } else if (n != 0 && n != Infinity && n != -Infinity) {
                    n = (n > 0 || -1) * Math.floor(Math.abs(n));
                }
            }
            if (n >= len) {
                return -1;
            }
            var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
            for (; k < len; k++) {
                if (k in t && t[k] === searchElement) {
                    return k;
                }
            }
            return -1;
        };
    }
    if (!String.prototype.trim) {
        String.prototype.trim = function() {
            return this.replace(/^\s+|\s+$/g, '');
        };
    }
    later.array = {};
    later.array.sort = function(arr, zeroIsLast) {
        arr.sort(function(a, b) {
            return +a - +b;
        });
        if (zeroIsLast && arr[0] === 0) {
            arr.push(arr.shift());
        }
    };
    later.array.next = function(val, values, extent) {
        var cur,
            zeroIsLargest = extent[0] !== 0,
            nextIdx = 0;
        for (var i = values.length - 1; i > -1; --i) {
            cur = values[i];
            if (cur === val) {
                return cur;
            }
            if (cur > val || (cur === 0 && zeroIsLargest && extent[1] > val)) {
                nextIdx = i;
                continue;
            }
            break;
        }
        return values[nextIdx];
    };
    later.array.nextInvalid = function(val, values, extent) {
        var min = extent[0],
            max = extent[1],
            len = values.length,
            zeroVal = values[len - 1] === 0 && min !== 0 ? max : 0,
            next = val,
            i = values.indexOf(val),
            start = next;
        while (next === (values[i] || zeroVal)) {
            next++;
            if (next > max) {
                next = min;
            }
            i++;
            if (i === len) {
                i = 0;
            }
            if (next === start) {
                return undefined;
            }
        }
        return next;
    };
    later.array.prev = function(val, values, extent) {
        var cur,
            len = values.length,
            zeroIsLargest = extent[0] !== 0,
            prevIdx = len - 1;
        for (var i = 0; i < len; i++) {
            cur = values[i];
            if (cur === val) {
                return cur;
            }
            if (cur < val || (cur === 0 && zeroIsLargest && extent[1] < val)) {
                prevIdx = i;
                continue;
            }
            break;
        }
        return values[prevIdx];
    };
    later.array.prevInvalid = function(val, values, extent) {
        var min = extent[0],
            max = extent[1],
            len = values.length,
            zeroVal = values[len - 1] === 0 && min !== 0 ? max : 0,
            next = val,
            i = values.indexOf(val),
            start = next;
        while (next === (values[i] || zeroVal)) {
            next--;
            if (next < min) {
                next = max;
            }
            i--;
            if (i === -1) {
                i = len - 1;
            }
            if (next === start) {
                return undefined;
            }
        }
        return next;
    };
    later.day = later.D = {
        name: 'day',
        range: 86400,
        val: function(d) {
            return d.D || (d.D = later.date.getDate.call(d));
        },
        isValid: function(d, val) {
            return later.D.val(d) === (val || later.D.extent(d)[1]);
        },
        extent: function(d) {
            if (d.DExtent) return d.DExtent;
            var month = later.M.val(d),
                max = later.DAYS_IN_MONTH[month - 1];
            if (month === 2 && later.dy.extent(d)[1] === 366) {
                max = max + 1;
            }
            return (d.DExtent = [1, max]);
        },
        start: function(d) {
            return (
                d.DStart ||
                (d.DStart = later.date.next(
                    later.Y.val(d),
                    later.M.val(d),
                    later.D.val(d)
                ))
            );
        },
        end: function(d) {
            return (
                d.DEnd ||
                (d.DEnd = later.date.prev(
                    later.Y.val(d),
                    later.M.val(d),
                    later.D.val(d)
                ))
            );
        },
        next: function(d, val) {
            val = val > later.D.extent(d)[1] ? 1 : val;
            var month = later.date.nextRollover(d, val, later.D, later.M),
                DMax = later.D.extent(month)[1];
            val = val > DMax ? 1 : val || DMax;
            return later.date.next(later.Y.val(month), later.M.val(month), val);
        },
        prev: function(d, val) {
            var month = later.date.prevRollover(d, val, later.D, later.M),
                DMax = later.D.extent(month)[1];
            return later.date.prev(
                later.Y.val(month),
                later.M.val(month),
                val > DMax ? DMax : val || DMax
            );
        }
    };
    later.dayOfWeekCount = later.dc = {
        name: 'day of week count',
        range: 604800,
        val: function(d) {
            return d.dc || (d.dc = Math.floor((later.D.val(d) - 1) / 7) + 1);
        },
        isValid: function(d, val) {
            return (
                later.dc.val(d) === val ||
                (val === 0 && later.D.val(d) > later.D.extent(d)[1] - 7)
            );
        },
        extent: function(d) {
            return (
                d.dcExtent ||
                (d.dcExtent = [1, Math.ceil(later.D.extent(d)[1] / 7)])
            );
        },
        start: function(d) {
            return (
                d.dcStart ||
                (d.dcStart = later.date.next(
                    later.Y.val(d),
                    later.M.val(d),
                    Math.max(1, (later.dc.val(d) - 1) * 7 + 1 || 1)
                ))
            );
        },
        end: function(d) {
            return (
                d.dcEnd ||
                (d.dcEnd = later.date.prev(
                    later.Y.val(d),
                    later.M.val(d),
                    Math.min(later.dc.val(d) * 7, later.D.extent(d)[1])
                ))
            );
        },
        next: function(d, val) {
            val = val > later.dc.extent(d)[1] ? 1 : val;
            var month = later.date.nextRollover(d, val, later.dc, later.M),
                dcMax = later.dc.extent(month)[1];
            val = val > dcMax ? 1 : val;
            var next = later.date.next(
                later.Y.val(month),
                later.M.val(month),
                val === 0 ? later.D.extent(month)[1] - 6 : 1 + 7 * (val - 1)
            );
            if (next.getTime() <= d.getTime()) {
                month = later.M.next(d, later.M.val(d) + 1);
                return later.date.next(
                    later.Y.val(month),
                    later.M.val(month),
                    val === 0 ? later.D.extent(month)[1] - 6 : 1 + 7 * (val - 1)
                );
            }
            return next;
        },
        prev: function(d, val) {
            var month = later.date.prevRollover(d, val, later.dc, later.M),
                dcMax = later.dc.extent(month)[1];
            val = val > dcMax ? dcMax : val || dcMax;
            return later.dc.end(
                later.date.prev(
                    later.Y.val(month),
                    later.M.val(month),
                    1 + 7 * (val - 1)
                )
            );
        }
    };
    later.dayOfWeek = later.dw = later.d = {
        name: 'day of week',
        range: 86400,
        val: function(d) {
            return d.dw || (d.dw = later.date.getDay.call(d) + 1);
        },
        isValid: function(d, val) {
            return later.dw.val(d) === (val || 7);
        },
        extent: function() {
            return [1, 7];
        },
        start: function(d) {
            return later.D.start(d);
        },
        end: function(d) {
            return later.D.end(d);
        },
        next: function(d, val) {
            val = val > 7 ? 1 : val || 7;
            return later.date.next(
                later.Y.val(d),
                later.M.val(d),
                later.D.val(d) +
                    (val - later.dw.val(d)) +
                    (val <= later.dw.val(d) ? 7 : 0)
            );
        },
        prev: function(d, val) {
            val = val > 7 ? 7 : val || 7;
            return later.date.prev(
                later.Y.val(d),
                later.M.val(d),
                later.D.val(d) +
                    (val - later.dw.val(d)) +
                    (val >= later.dw.val(d) ? -7 : 0)
            );
        }
    };
    later.dayOfYear = later.dy = {
        name: 'day of year',
        range: 86400,
        val: function(d) {
            return (
                d.dy ||
                (d.dy = Math.ceil(
                    1 +
                        (later.D.start(d).getTime() -
                            later.Y.start(d).getTime()) /
                            later.DAY
                ))
            );
        },
        isValid: function(d, val) {
            return later.dy.val(d) === (val || later.dy.extent(d)[1]);
        },
        extent: function(d) {
            var year = later.Y.val(d);
            return d.dyExtent || (d.dyExtent = [1, year % 4 ? 365 : 366]);
        },
        start: function(d) {
            return later.D.start(d);
        },
        end: function(d) {
            return later.D.end(d);
        },
        next: function(d, val) {
            val = val > later.dy.extent(d)[1] ? 1 : val;
            var year = later.date.nextRollover(d, val, later.dy, later.Y),
                dyMax = later.dy.extent(year)[1];
            val = val > dyMax ? 1 : val || dyMax;
            return later.date.next(later.Y.val(year), later.M.val(year), val);
        },
        prev: function(d, val) {
            var year = later.date.prevRollover(d, val, later.dy, later.Y),
                dyMax = later.dy.extent(year)[1];
            val = val > dyMax ? dyMax : val || dyMax;
            return later.date.prev(later.Y.val(year), later.M.val(year), val);
        }
    };
    later.hour = later.h = {
        name: 'hour',
        range: 3600,
        val: function(d) {
            return d.h || (d.h = later.date.getHour.call(d));
        },
        isValid: function(d, val) {
            return later.h.val(d) === val;
        },
        extent: function() {
            return [0, 23];
        },
        start: function(d) {
            return (
                d.hStart ||
                (d.hStart = later.date.next(
                    later.Y.val(d),
                    later.M.val(d),
                    later.D.val(d),
                    later.h.val(d)
                ))
            );
        },
        end: function(d) {
            return (
                d.hEnd ||
                (d.hEnd = later.date.prev(
                    later.Y.val(d),
                    later.M.val(d),
                    later.D.val(d),
                    later.h.val(d)
                ))
            );
        },
        next: function(d, val) {
            val = val > 23 ? 0 : val;
            var next = later.date.next(
                later.Y.val(d),
                later.M.val(d),
                later.D.val(d) + (val <= later.h.val(d) ? 1 : 0),
                val
            );
            if (!later.date.isUTC && next.getTime() <= d.getTime()) {
                next = later.date.next(
                    later.Y.val(next),
                    later.M.val(next),
                    later.D.val(next),
                    val + 1
                );
            }
            return next;
        },
        prev: function(d, val) {
            val = val > 23 ? 23 : val;
            return later.date.prev(
                later.Y.val(d),
                later.M.val(d),
                later.D.val(d) + (val >= later.h.val(d) ? -1 : 0),
                val
            );
        }
    };
    later.minute = later.m = {
        name: 'minute',
        range: 60,
        val: function(d) {
            return d.m || (d.m = later.date.getMin.call(d));
        },
        isValid: function(d, val) {
            return later.m.val(d) === val;
        },
        extent: function() {
            return [0, 59];
        },
        start: function(d) {
            return (
                d.mStart ||
                (d.mStart = later.date.next(
                    later.Y.val(d),
                    later.M.val(d),
                    later.D.val(d),
                    later.h.val(d),
                    later.m.val(d)
                ))
            );
        },
        end: function(d) {
            return (
                d.mEnd ||
                (d.mEnd = later.date.prev(
                    later.Y.val(d),
                    later.M.val(d),
                    later.D.val(d),
                    later.h.val(d),
                    later.m.val(d)
                ))
            );
        },
        next: function(d, val) {
            var m = later.m.val(d),
                s = later.s.val(d),
                inc = val > 59 ? 60 - m : val <= m ? 60 - m + val : val - m,
                next = new Date(d.getTime() + inc * later.MIN - s * later.SEC);
            if (!later.date.isUTC && next.getTime() <= d.getTime()) {
                next = new Date(
                    d.getTime() + (inc + 120) * later.MIN - s * later.SEC
                );
            }
            return next;
        },
        prev: function(d, val) {
            val = val > 59 ? 59 : val;
            return later.date.prev(
                later.Y.val(d),
                later.M.val(d),
                later.D.val(d),
                later.h.val(d) + (val >= later.m.val(d) ? -1 : 0),
                val
            );
        }
    };
    later.month = later.M = {
        name: 'month',
        range: 2629740,
        val: function(d) {
            return d.M || (d.M = later.date.getMonth.call(d) + 1);
        },
        isValid: function(d, val) {
            return later.M.val(d) === (val || 12);
        },
        extent: function() {
            return [1, 12];
        },
        start: function(d) {
            return (
                d.MStart ||
                (d.MStart = later.date.next(later.Y.val(d), later.M.val(d)))
            );
        },
        end: function(d) {
            return (
                d.MEnd ||
                (d.MEnd = later.date.prev(later.Y.val(d), later.M.val(d)))
            );
        },
        next: function(d, val) {
            val = val > 12 ? 1 : val || 12;
            return later.date.next(
                later.Y.val(d) + (val > later.M.val(d) ? 0 : 1),
                val
            );
        },
        prev: function(d, val) {
            val = val > 12 ? 12 : val || 12;
            return later.date.prev(
                later.Y.val(d) - (val >= later.M.val(d) ? 1 : 0),
                val
            );
        }
    };
    later.second = later.s = {
        name: 'second',
        range: 1,
        val: function(d) {
            return d.s || (d.s = later.date.getSec.call(d));
        },
        isValid: function(d, val) {
            return later.s.val(d) === val;
        },
        extent: function() {
            return [0, 59];
        },
        start: function(d) {
            return d;
        },
        end: function(d) {
            return d;
        },
        next: function(d, val) {
            var s = later.s.val(d),
                inc = val > 59 ? 60 - s : val <= s ? 60 - s + val : val - s,
                next = new Date(d.getTime() + inc * later.SEC);
            if (!later.date.isUTC && next.getTime() <= d.getTime()) {
                next = new Date(d.getTime() + (inc + 7200) * later.SEC);
            }
            return next;
        },
        prev: function(d, val) {
            val = val > 59 ? 59 : val;
            return later.date.prev(
                later.Y.val(d),
                later.M.val(d),
                later.D.val(d),
                later.h.val(d),
                later.m.val(d) + (val >= later.s.val(d) ? -1 : 0),
                val
            );
        }
    };
    later.time = later.t = {
        name: 'time',
        range: 1,
        val: function(d) {
            return (
                d.t ||
                (d.t =
                    later.h.val(d) * 3600 +
                    later.m.val(d) * 60 +
                    later.s.val(d))
            );
        },
        isValid: function(d, val) {
            return later.t.val(d) === val;
        },
        extent: function() {
            return [0, 86399];
        },
        start: function(d) {
            return d;
        },
        end: function(d) {
            return d;
        },
        next: function(d, val) {
            val = val > 86399 ? 0 : val;
            var next = later.date.next(
                later.Y.val(d),
                later.M.val(d),
                later.D.val(d) + (val <= later.t.val(d) ? 1 : 0),
                0,
                0,
                val
            );
            if (!later.date.isUTC && next.getTime() < d.getTime()) {
                next = later.date.next(
                    later.Y.val(next),
                    later.M.val(next),
                    later.D.val(next),
                    later.h.val(next),
                    later.m.val(next),
                    val + 7200
                );
            }
            return next;
        },
        prev: function(d, val) {
            val = val > 86399 ? 86399 : val;
            return later.date.next(
                later.Y.val(d),
                later.M.val(d),
                later.D.val(d) + (val >= later.t.val(d) ? -1 : 0),
                0,
                0,
                val
            );
        }
    };
    later.weekOfMonth = later.wm = {
        name: 'week of month',
        range: 604800,
        val: function(d) {
            return (
                d.wm ||
                (d.wm =
                    (later.D.val(d) +
                        (later.dw.val(later.M.start(d)) - 1) +
                        (7 - later.dw.val(d))) /
                    7)
            );
        },
        isValid: function(d, val) {
            return later.wm.val(d) === (val || later.wm.extent(d)[1]);
        },
        extent: function(d) {
            return (
                d.wmExtent ||
                (d.wmExtent = [
                    1,
                    (later.D.extent(d)[1] +
                        (later.dw.val(later.M.start(d)) - 1) +
                        (7 - later.dw.val(later.M.end(d)))) /
                        7
                ])
            );
        },
        start: function(d) {
            return (
                d.wmStart ||
                (d.wmStart = later.date.next(
                    later.Y.val(d),
                    later.M.val(d),
                    Math.max(later.D.val(d) - later.dw.val(d) + 1, 1)
                ))
            );
        },
        end: function(d) {
            return (
                d.wmEnd ||
                (d.wmEnd = later.date.prev(
                    later.Y.val(d),
                    later.M.val(d),
                    Math.min(
                        later.D.val(d) + (7 - later.dw.val(d)),
                        later.D.extent(d)[1]
                    )
                ))
            );
        },
        next: function(d, val) {
            val = val > later.wm.extent(d)[1] ? 1 : val;
            var month = later.date.nextRollover(d, val, later.wm, later.M),
                wmMax = later.wm.extent(month)[1];
            val = val > wmMax ? 1 : val || wmMax;
            return later.date.next(
                later.Y.val(month),
                later.M.val(month),
                Math.max(1, (val - 1) * 7 - (later.dw.val(month) - 2))
            );
        },
        prev: function(d, val) {
            var month = later.date.prevRollover(d, val, later.wm, later.M),
                wmMax = later.wm.extent(month)[1];
            val = val > wmMax ? wmMax : val || wmMax;
            return later.wm.end(
                later.date.next(
                    later.Y.val(month),
                    later.M.val(month),
                    Math.max(1, (val - 1) * 7 - (later.dw.val(month) - 2))
                )
            );
        }
    };
    later.weekOfYear = later.wy = {
        name: 'week of year (ISO)',
        range: 604800,
        val: function(d) {
            if (d.wy) return d.wy;
            var wThur = later.dw.next(later.wy.start(d), 5),
                YThur = later.dw.next(
                    later.Y.prev(wThur, later.Y.val(wThur) - 1),
                    5
                );
            return (d.wy =
                1 +
                Math.ceil((wThur.getTime() - YThur.getTime()) / later.WEEK));
        },
        isValid: function(d, val) {
            return later.wy.val(d) === (val || later.wy.extent(d)[1]);
        },
        extent: function(d) {
            if (d.wyExtent) return d.wyExtent;
            var year = later.dw.next(later.wy.start(d), 5),
                dwFirst = later.dw.val(later.Y.start(year)),
                dwLast = later.dw.val(later.Y.end(year));
            return (d.wyExtent = [1, dwFirst === 5 || dwLast === 5 ? 53 : 52]);
        },
        start: function(d) {
            return (
                d.wyStart ||
                (d.wyStart = later.date.next(
                    later.Y.val(d),
                    later.M.val(d),
                    later.D.val(d) -
                        (later.dw.val(d) > 1 ? later.dw.val(d) - 2 : 6)
                ))
            );
        },
        end: function(d) {
            return (
                d.wyEnd ||
                (d.wyEnd = later.date.prev(
                    later.Y.val(d),
                    later.M.val(d),
                    later.D.val(d) +
                        (later.dw.val(d) > 1 ? 8 - later.dw.val(d) : 0)
                ))
            );
        },
        next: function(d, val) {
            val = val > later.wy.extent(d)[1] ? 1 : val;
            var wyThur = later.dw.next(later.wy.start(d), 5),
                year = later.date.nextRollover(wyThur, val, later.wy, later.Y);
            if (later.wy.val(year) !== 1) {
                year = later.dw.next(year, 2);
            }
            var wyMax = later.wy.extent(year)[1],
                wyStart = later.wy.start(year);
            val = val > wyMax ? 1 : val || wyMax;
            return later.date.next(
                later.Y.val(wyStart),
                later.M.val(wyStart),
                later.D.val(wyStart) + 7 * (val - 1)
            );
        },
        prev: function(d, val) {
            var wyThur = later.dw.next(later.wy.start(d), 5),
                year = later.date.prevRollover(wyThur, val, later.wy, later.Y);
            if (later.wy.val(year) !== 1) {
                year = later.dw.next(year, 2);
            }
            var wyMax = later.wy.extent(year)[1],
                wyEnd = later.wy.end(year);
            val = val > wyMax ? wyMax : val || wyMax;
            return later.wy.end(
                later.date.next(
                    later.Y.val(wyEnd),
                    later.M.val(wyEnd),
                    later.D.val(wyEnd) + 7 * (val - 1)
                )
            );
        }
    };
    later.year = later.Y = {
        name: 'year',
        range: 31556900,
        val: function(d) {
            return d.Y || (d.Y = later.date.getYear.call(d));
        },
        isValid: function(d, val) {
            return later.Y.val(d) === val;
        },
        extent: function() {
            return [1970, 2099];
        },
        start: function(d) {
            return d.YStart || (d.YStart = later.date.next(later.Y.val(d)));
        },
        end: function(d) {
            return d.YEnd || (d.YEnd = later.date.prev(later.Y.val(d)));
        },
        next: function(d, val) {
            return val > later.Y.val(d) && val <= later.Y.extent()[1]
                ? later.date.next(val)
                : later.NEVER;
        },
        prev: function(d, val) {
            return val < later.Y.val(d) && val >= later.Y.extent()[0]
                ? later.date.prev(val)
                : later.NEVER;
        }
    };
    later.fullDate = later.fd = {
        name: 'full date',
        range: 1,
        val: function(d) {
            return d.fd || (d.fd = d.getTime());
        },
        isValid: function(d, val) {
            return later.fd.val(d) === val;
        },
        extent: function() {
            return [0, 3250368e7];
        },
        start: function(d) {
            return d;
        },
        end: function(d) {
            return d;
        },
        next: function(d, val) {
            return later.fd.val(d) < val ? new Date(val) : later.NEVER;
        },
        prev: function(d, val) {
            return later.fd.val(d) > val ? new Date(val) : later.NEVER;
        }
    };
    later.modifier = {};
    later.modifier.after = later.modifier.a = function(constraint, values) {
        var value = values[0];
        return {
            name: 'after ' + constraint.name,
            range:
                (constraint.extent(new Date())[1] - value) * constraint.range,
            val: constraint.val,
            isValid: function(d) {
                return this.val(d) >= value;
            },
            extent: constraint.extent,
            start: constraint.start,
            end: constraint.end,
            next: function(startDate, val) {
                if (val != value) val = constraint.extent(startDate)[0];
                return constraint.next(startDate, val);
            },
            prev: function(startDate, val) {
                val =
                    val === value ? constraint.extent(startDate)[1] : value - 1;
                return constraint.prev(startDate, val);
            }
        };
    };
    later.modifier.before = later.modifier.b = function(constraint, values) {
        var value = values[values.length - 1];
        return {
            name: 'before ' + constraint.name,
            range: constraint.range * (value - 1),
            val: constraint.val,
            isValid: function(d) {
                return this.val(d) < value;
            },
            extent: constraint.extent,
            start: constraint.start,
            end: constraint.end,
            next: function(startDate, val) {
                val = val === value ? constraint.extent(startDate)[0] : value;
                return constraint.next(startDate, val);
            },
            prev: function(startDate, val) {
                val =
                    val === value ? value - 1 : constraint.extent(startDate)[1];
                return constraint.prev(startDate, val);
            }
        };
    };
    later.compile = function(schedDef) {
        var constraints = [],
            constraintsLen = 0,
            tickConstraint;
        for (var key in schedDef) {
            var nameParts = key.split('_'),
                name = nameParts[0],
                mod = nameParts[1],
                vals = schedDef[key],
                constraint = mod
                    ? later.modifier[mod](later[name], vals)
                    : later[name];
            constraints.push({
                constraint: constraint,
                vals: vals
            });
            constraintsLen++;
        }
        constraints.sort(function(a, b) {
            var ra = a.constraint.range,
                rb = b.constraint.range;
            return rb < ra ? -1 : rb > ra ? 1 : 0;
        });
        tickConstraint = constraints[constraintsLen - 1].constraint;

        function compareFn(dir) {
            return dir === 'next'
                ? function(a, b) {
                      return a.getTime() > b.getTime();
                  }
                : function(a, b) {
                      return b.getTime() > a.getTime();
                  };
        }
        return {
            start: function(dir, startDate) {
                var next = startDate,
                    nextVal = later.array[dir],
                    maxAttempts = 1e3,
                    done;
                while (maxAttempts-- && !done && next) {
                    done = true;
                    for (var i = 0; i < constraintsLen; i++) {
                        var constraint = constraints[i].constraint,
                            curVal = constraint.val(next),
                            extent = constraint.extent(next),
                            newVal = nextVal(
                                curVal,
                                constraints[i].vals,
                                extent
                            );
                        if (!constraint.isValid(next, newVal)) {
                            next = constraint[dir](next, newVal);
                            done = false;
                            break;
                        }
                    }
                }
                if (next !== later.NEVER) {
                    next =
                        dir === 'next'
                            ? tickConstraint.start(next)
                            : tickConstraint.end(next);
                }
                return next;
            },
            end: function(dir, startDate) {
                var result,
                    nextVal = later.array[dir + 'Invalid'],
                    compare = compareFn(dir);
                for (var i = constraintsLen - 1; i >= 0; i--) {
                    var constraint = constraints[i].constraint,
                        curVal = constraint.val(startDate),
                        extent = constraint.extent(startDate),
                        newVal = nextVal(curVal, constraints[i].vals, extent),
                        next;
                    if (newVal !== undefined) {
                        next = constraint[dir](startDate, newVal);
                        if (next && (!result || compare(result, next))) {
                            result = next;
                        }
                    }
                }
                return result;
            },
            tick: function(dir, date) {
                return new Date(
                    dir === 'next'
                        ? tickConstraint.end(date).getTime() + later.SEC
                        : tickConstraint.start(date).getTime() - later.SEC
                );
            },
            tickStart: function(date) {
                return tickConstraint.start(date);
            }
        };
    };
    later.schedule = function(sched) {
        if (!sched) throw new Error('Missing schedule definition.');
        if (!sched.schedules)
            throw new Error('Definition must include at least one schedule.');
        var schedules = [],
            schedulesLen = sched.schedules.length,
            exceptions = [],
            exceptionsLen = sched.exceptions ? sched.exceptions.length : 0;
        for (var i = 0; i < schedulesLen; i++) {
            schedules.push(later.compile(sched.schedules[i]));
        }
        for (var j = 0; j < exceptionsLen; j++) {
            exceptions.push(later.compile(sched.exceptions[j]));
        }

        function getInstances(dir, count, startDate, endDate, isRange) {
            var compare = compareFn(dir),
                loopCount = count,
                maxAttempts = 1e3,
                schedStarts = [],
                exceptStarts = [],
                next,
                end,
                results = [],
                isForward = dir === 'next',
                lastResult,
                rStart = isForward ? 0 : 1,
                rEnd = isForward ? 1 : 0;
            startDate = startDate ? new Date(startDate) : new Date();
            if (!startDate || !startDate.getTime())
                throw new Error('Invalid start date.');
            setNextStarts(dir, schedules, schedStarts, startDate);
            setRangeStarts(dir, exceptions, exceptStarts, startDate);
            while (
                maxAttempts-- &&
                loopCount &&
                (next = findNext(schedStarts, compare))
            ) {
                if (endDate && compare(next, endDate)) {
                    break;
                }
                if (exceptionsLen) {
                    updateRangeStarts(dir, exceptions, exceptStarts, next);
                    if ((end = calcRangeOverlap(dir, exceptStarts, next))) {
                        updateNextStarts(dir, schedules, schedStarts, end);
                        continue;
                    }
                }
                if (isRange) {
                    var maxEndDate = calcMaxEndDate(exceptStarts, compare);
                    end = calcEnd(
                        dir,
                        schedules,
                        schedStarts,
                        next,
                        maxEndDate
                    );
                    var r = isForward
                        ? [
                              new Date(Math.max(startDate, next)),
                              end
                                  ? new Date(
                                        endDate ? Math.min(end, endDate) : end
                                    )
                                  : undefined
                          ]
                        : [
                              end
                                  ? new Date(
                                        endDate
                                            ? Math.max(
                                                  endDate,
                                                  end.getTime() + later.SEC
                                              )
                                            : end.getTime() + later.SEC
                                    )
                                  : undefined,
                              new Date(
                                  Math.min(
                                      startDate,
                                      next.getTime() + later.SEC
                                  )
                              )
                          ];
                    if (
                        lastResult &&
                        r[rStart].getTime() === lastResult[rEnd].getTime()
                    ) {
                        lastResult[rEnd] = r[rEnd];
                        loopCount++;
                    } else {
                        lastResult = r;
                        results.push(lastResult);
                    }
                    if (!end) break;
                    updateNextStarts(dir, schedules, schedStarts, end);
                } else {
                    results.push(
                        isForward
                            ? new Date(Math.max(startDate, next))
                            : getStart(schedules, schedStarts, next, endDate)
                    );
                    tickStarts(dir, schedules, schedStarts, next);
                }
                loopCount--;
            }
            for (var i = 0, len = results.length; i < len; i++) {
                var result = results[i];
                results[i] =
                    Object.prototype.toString.call(result) === '[object Array]'
                        ? [cleanDate(result[0]), cleanDate(result[1])]
                        : cleanDate(result);
            }
            return results.length === 0
                ? later.NEVER
                : count === 1
                    ? results[0]
                    : results;
        }

        function cleanDate(d) {
            if (d instanceof Date && !isNaN(d.valueOf())) {
                return new Date(d);
            }
            return undefined;
        }

        function setNextStarts(dir, schedArr, startsArr, startDate) {
            for (var i = 0, len = schedArr.length; i < len; i++) {
                startsArr[i] = schedArr[i].start(dir, startDate);
            }
        }

        function updateNextStarts(dir, schedArr, startsArr, startDate) {
            var compare = compareFn(dir);
            for (var i = 0, len = schedArr.length; i < len; i++) {
                if (startsArr[i] && !compare(startsArr[i], startDate)) {
                    startsArr[i] = schedArr[i].start(dir, startDate);
                }
            }
        }

        function setRangeStarts(dir, schedArr, rangesArr, startDate) {
            for (var i = 0, len = schedArr.length; i < len; i++) {
                var nextStart = schedArr[i].start(dir, startDate);
                if (!nextStart) {
                    rangesArr[i] = later.NEVER;
                } else {
                    rangesArr[i] = [nextStart, schedArr[i].end(dir, nextStart)];
                }
            }
        }

        function updateRangeStarts(dir, schedArr, rangesArr, startDate) {
            var compare = compareFn(dir);
            for (var i = 0, len = schedArr.length; i < len; i++) {
                if (rangesArr[i] && !compare(rangesArr[i][0], startDate)) {
                    var nextStart = schedArr[i].start(dir, startDate);
                    if (!nextStart) {
                        rangesArr[i] = later.NEVER;
                    } else {
                        rangesArr[i] = [
                            nextStart,
                            schedArr[i].end(dir, nextStart)
                        ];
                    }
                }
            }
        }

        function tickStarts(dir, schedArr, startsArr, startDate) {
            for (var i = 0, len = schedArr.length; i < len; i++) {
                if (
                    startsArr[i] &&
                    startsArr[i].getTime() === startDate.getTime()
                ) {
                    startsArr[i] = schedArr[i].start(
                        dir,
                        schedArr[i].tick(dir, startDate)
                    );
                }
            }
        }

        function getStart(schedArr, startsArr, startDate, minEndDate) {
            var result;
            for (var i = 0, len = startsArr.length; i < len; i++) {
                if (
                    startsArr[i] &&
                    startsArr[i].getTime() === startDate.getTime()
                ) {
                    var start = schedArr[i].tickStart(startDate);
                    if (minEndDate && start < minEndDate) {
                        return minEndDate;
                    }
                    if (!result || start > result) {
                        result = start;
                    }
                }
            }
            return result;
        }

        function calcRangeOverlap(dir, rangesArr, startDate) {
            var compare = compareFn(dir),
                result;
            for (var i = 0, len = rangesArr.length; i < len; i++) {
                var range = rangesArr[i];
                if (
                    range &&
                    !compare(range[0], startDate) &&
                    (!range[1] || compare(range[1], startDate))
                ) {
                    if (!result || compare(range[1], result)) {
                        result = range[1];
                    }
                }
            }
            return result;
        }

        function calcMaxEndDate(exceptsArr, compare) {
            var result;
            for (var i = 0, len = exceptsArr.length; i < len; i++) {
                if (
                    exceptsArr[i] &&
                    (!result || compare(result, exceptsArr[i][0]))
                ) {
                    result = exceptsArr[i][0];
                }
            }
            return result;
        }

        function calcEnd(dir, schedArr, startsArr, startDate, maxEndDate) {
            var compare = compareFn(dir),
                result;
            for (var i = 0, len = schedArr.length; i < len; i++) {
                var start = startsArr[i];
                if (start && start.getTime() === startDate.getTime()) {
                    var end = schedArr[i].end(dir, start);
                    if (maxEndDate && (!end || compare(end, maxEndDate))) {
                        return maxEndDate;
                    }
                    if (!result || compare(end, result)) {
                        result = end;
                    }
                }
            }
            return result;
        }

        function compareFn(dir) {
            return dir === 'next'
                ? function(a, b) {
                      return !b || a.getTime() > b.getTime();
                  }
                : function(a, b) {
                      return !a || b.getTime() > a.getTime();
                  };
        }

        function findNext(arr, compare) {
            var next = arr[0];
            for (var i = 1, len = arr.length; i < len; i++) {
                if (arr[i] && compare(next, arr[i])) {
                    next = arr[i];
                }
            }
            return next;
        }
        return {
            isValid: function(d) {
                return getInstances('next', 1, d, d) !== later.NEVER;
            },
            next: function(count, startDate, endDate) {
                return getInstances('next', count || 1, startDate, endDate);
            },
            prev: function(count, startDate, endDate) {
                return getInstances('prev', count || 1, startDate, endDate);
            },
            nextRange: function(count, startDate, endDate) {
                return getInstances(
                    'next',
                    count || 1,
                    startDate,
                    endDate,
                    true
                );
            },
            prevRange: function(count, startDate, endDate) {
                return getInstances(
                    'prev',
                    count || 1,
                    startDate,
                    endDate,
                    true
                );
            }
        };
    };
    later.setTimeout = function(fn, sched) {
        var s = later.schedule(sched),
            t;
        if (fn) {
            scheduleTimeout();
        }

        function scheduleTimeout() {
            var now = Date.now(),
                next = s.next(2, now);
            if (!next[0]) {
                t = undefined;
                return;
            }
            var diff = next[0].getTime() - now;
            if (diff < 1e3) {
                diff = next[1] ? next[1].getTime() - now : 1e3;
            }
            if (diff < 2147483647) {
                t = setTimeout(fn, diff);
            } else {
                t = setTimeout(scheduleTimeout, 2147483647);
            }
        }
        return {
            isDone: function() {
                return !t;
            },
            clear: function() {
                clearTimeout(t);
            }
        };
    };
    later.setInterval = function(fn, sched) {
        if (!fn) {
            return;
        }
        var t = later.setTimeout(scheduleTimeout, sched),
            done = t.isDone();

        function scheduleTimeout() {
            if (!done) {
                fn();
                t = later.setTimeout(scheduleTimeout, sched);
            }
        }
        return {
            isDone: function() {
                return t.isDone();
            },
            clear: function() {
                done = true;
                t.clear();
            }
        };
    };
    later.date = {};
    later.date.timezone = function(useLocalTime) {
        later.date.build = useLocalTime
            ? function(Y, M, D, h, m, s) {
                  return new Date(Y, M, D, h, m, s);
              }
            : function(Y, M, D, h, m, s) {
                  return new Date(Date.UTC(Y, M, D, h, m, s));
              };
        var get = useLocalTime ? 'get' : 'getUTC',
            d = Date.prototype;
        later.date.getYear = d[get + 'FullYear'];
        later.date.getMonth = d[get + 'Month'];
        later.date.getDate = d[get + 'Date'];
        later.date.getDay = d[get + 'Day'];
        later.date.getHour = d[get + 'Hours'];
        later.date.getMin = d[get + 'Minutes'];
        later.date.getSec = d[get + 'Seconds'];
        later.date.isUTC = !useLocalTime;
    };
    later.date.UTC = function() {
        later.date.timezone(false);
    };
    later.date.localTime = function() {
        later.date.timezone(true);
    };
    later.date.UTC();
    later.SEC = 1e3;
    later.MIN = later.SEC * 60;
    later.HOUR = later.MIN * 60;
    later.DAY = later.HOUR * 24;
    later.WEEK = later.DAY * 7;
    later.DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    later.NEVER = 0;
    later.date.next = function(Y, M, D, h, m, s) {
        return later.date.build(
            Y,
            M !== undefined ? M - 1 : 0,
            D !== undefined ? D : 1,
            h || 0,
            m || 0,
            s || 0
        );
    };
    later.date.nextRollover = function(d, val, constraint, period) {
        var cur = constraint.val(d),
            max = constraint.extent(d)[1];
        return (val || max) <= cur || val > max
            ? new Date(period.end(d).getTime() + later.SEC)
            : period.start(d);
    };
    later.date.prev = function(Y, M, D, h, m, s) {
        var len = arguments.length;
        M = len < 2 ? 11 : M - 1;
        D = len < 3 ? later.D.extent(later.date.next(Y, M + 1))[1] : D;
        h = len < 4 ? 23 : h;
        m = len < 5 ? 59 : m;
        s = len < 6 ? 59 : s;
        return later.date.build(Y, M, D, h, m, s);
    };
    later.date.prevRollover = function(d, val, constraint, period) {
        var cur = constraint.val(d);
        return val >= cur || !val
            ? period.start(period.prev(d, period.val(d) - 1))
            : period.start(d);
    };
    later.parse = {};
    later.parse.cron = function(expr, hasSeconds) {
        var NAMES = {
            JAN: 1,
            FEB: 2,
            MAR: 3,
            APR: 4,
            MAY: 5,
            JUN: 6,
            JUL: 7,
            AUG: 8,
            SEP: 9,
            OCT: 10,
            NOV: 11,
            DEC: 12,
            SUN: 1,
            MON: 2,
            TUE: 3,
            WED: 4,
            THU: 5,
            FRI: 6,
            SAT: 7
        };
        var REPLACEMENTS = {
            '* * * * * *': '0/1 * * * * *',
            '@YEARLY': '0 0 1 1 *',
            '@ANNUALLY': '0 0 1 1 *',
            '@MONTHLY': '0 0 1 * *',
            '@WEEKLY': '0 0 * * 0',
            '@DAILY': '0 0 * * *',
            '@HOURLY': '0 * * * *'
        };
        var FIELDS = {
            s: [0, 0, 59],
            m: [1, 0, 59],
            h: [2, 0, 23],
            D: [3, 1, 31],
            M: [4, 1, 12],
            Y: [6, 1970, 2099],
            d: [5, 1, 7, 1]
        };

        function getValue(value, offset, max) {
            return isNaN(value)
                ? NAMES[value] || null
                : Math.min(+value + (offset || 0), max || 9999);
        }

        function cloneSchedule(sched) {
            var clone = {},
                field;
            for (field in sched) {
                if (field !== 'dc' && field !== 'd') {
                    clone[field] = sched[field].slice(0);
                }
            }
            return clone;
        }

        function add(sched, name, min, max, inc) {
            var i = min;
            if (!sched[name]) {
                sched[name] = [];
            }
            while (i <= max) {
                if (sched[name].indexOf(i) < 0) {
                    sched[name].push(i);
                }
                i += inc || 1;
            }
            sched[name].sort(function(a, b) {
                return a - b;
            });
        }

        function addHash(schedules, curSched, value, hash) {
            if (
                (curSched.d && !curSched.dc) ||
                (curSched.dc && curSched.dc.indexOf(hash) < 0)
            ) {
                schedules.push(cloneSchedule(curSched));
                curSched = schedules[schedules.length - 1];
            }
            add(curSched, 'd', value, value);
            add(curSched, 'dc', hash, hash);
        }

        function addWeekday(s, curSched, value) {
            var except1 = {},
                except2 = {};
            if (value === 1) {
                add(curSched, 'D', 1, 3);
                add(curSched, 'd', NAMES.MON, NAMES.FRI);
                add(except1, 'D', 2, 2);
                add(except1, 'd', NAMES.TUE, NAMES.FRI);
                add(except2, 'D', 3, 3);
                add(except2, 'd', NAMES.TUE, NAMES.FRI);
            } else {
                add(curSched, 'D', value - 1, value + 1);
                add(curSched, 'd', NAMES.MON, NAMES.FRI);
                add(except1, 'D', value - 1, value - 1);
                add(except1, 'd', NAMES.MON, NAMES.THU);
                add(except2, 'D', value + 1, value + 1);
                add(except2, 'd', NAMES.TUE, NAMES.FRI);
            }
            s.exceptions.push(except1);
            s.exceptions.push(except2);
        }

        function addRange(item, curSched, name, min, max, offset) {
            var incSplit = item.split('/'),
                inc = +incSplit[1],
                range = incSplit[0];
            if (range !== '*' && range !== '0') {
                var rangeSplit = range.split('-');
                min = getValue(rangeSplit[0], offset, max);
                max = getValue(rangeSplit[1], offset, max) || max;
            }
            add(curSched, name, min, max, inc);
        }

        function parse(item, s, name, min, max, offset) {
            var value,
                split,
                schedules = s.schedules,
                curSched = schedules[schedules.length - 1];
            if (item === 'L') {
                item = min - 1;
            }
            if ((value = getValue(item, offset, max)) !== null) {
                add(curSched, name, value, value);
            } else if (
                (value = getValue(item.replace('W', ''), offset, max)) !== null
            ) {
                addWeekday(s, curSched, value);
            } else if (
                (value = getValue(item.replace('L', ''), offset, max)) !== null
            ) {
                addHash(schedules, curSched, value, min - 1);
            } else if ((split = item.split('#')).length === 2) {
                value = getValue(split[0], offset, max);
                addHash(schedules, curSched, value, getValue(split[1]));
            } else {
                addRange(item, curSched, name, min, max, offset);
            }
        }

        function isHash(item) {
            return item.indexOf('#') > -1 || item.indexOf('L') > 0;
        }

        function itemSorter(a, b) {
            return isHash(a) && !isHash(b) ? 1 : a - b;
        }

        function parseExpr(expr) {
            var schedule = {
                    schedules: [{}],
                    exceptions: []
                },
                components = expr.replace(/(\s)+/g, ' ').split(' '),
                field,
                f,
                component,
                items;
            for (field in FIELDS) {
                f = FIELDS[field];
                component = components[f[0]];
                if (component && component !== '*' && component !== '?') {
                    items = component.split(',').sort(itemSorter);
                    var i,
                        length = items.length;
                    for (i = 0; i < length; i++) {
                        parse(items[i], schedule, field, f[1], f[2], f[3]);
                    }
                }
            }
            return schedule;
        }

        function prepareExpr(expr) {
            var prepared = expr.toUpperCase();
            return REPLACEMENTS[prepared] || prepared;
        }
        var e = prepareExpr(expr);
        return parseExpr(hasSeconds ? e : '0 ' + e);
    };
    later.parse.recur = function() {
        var schedules = [],
            exceptions = [],
            cur,
            curArr = schedules,
            curName,
            values,
            every,
            modifier,
            applyMin,
            applyMax,
            i,
            last;

        function add(name, min, max) {
            name = modifier ? name + '_' + modifier : name;
            if (!cur) {
                curArr.push({});
                cur = curArr[0];
            }
            if (!cur[name]) {
                cur[name] = [];
            }
            curName = cur[name];
            if (every) {
                values = [];
                for (i = min; i <= max; i += every) {
                    values.push(i);
                }
                last = {
                    n: name,
                    x: every,
                    c: curName.length,
                    m: max
                };
            }
            values = applyMin ? [min] : applyMax ? [max] : values;
            var length = values.length;
            for (i = 0; i < length; i += 1) {
                var val = values[i];
                if (curName.indexOf(val) < 0) {
                    curName.push(val);
                }
            }
            values = every = modifier = applyMin = applyMax = 0;
        }
        return {
            schedules: schedules,
            exceptions: exceptions,
            on: function() {
                values =
                    arguments[0] instanceof Array ? arguments[0] : arguments;
                return this;
            },
            every: function(x) {
                every = x || 1;
                return this;
            },
            after: function(x) {
                modifier = 'a';
                values = [x];
                return this;
            },
            before: function(x) {
                modifier = 'b';
                values = [x];
                return this;
            },
            first: function() {
                applyMin = 1;
                return this;
            },
            last: function() {
                applyMax = 1;
                return this;
            },
            time: function() {
                for (var i = 0, len = values.length; i < len; i++) {
                    var split = values[i].split(':');
                    if (split.length < 3) split.push(0);
                    values[i] = +split[0] * 3600 + +split[1] * 60 + +split[2];
                }
                add('t');
                return this;
            },
            second: function() {
                add('s', 0, 59);
                return this;
            },
            minute: function() {
                add('m', 0, 59);
                return this;
            },
            hour: function() {
                add('h', 0, 23);
                return this;
            },
            dayOfMonth: function() {
                add('D', 1, applyMax ? 0 : 31);
                return this;
            },
            dayOfWeek: function() {
                add('d', 1, 7);
                return this;
            },
            onWeekend: function() {
                values = [1, 7];
                return this.dayOfWeek();
            },
            onWeekday: function() {
                values = [2, 3, 4, 5, 6];
                return this.dayOfWeek();
            },
            dayOfWeekCount: function() {
                add('dc', 1, applyMax ? 0 : 5);
                return this;
            },
            dayOfYear: function() {
                add('dy', 1, applyMax ? 0 : 366);
                return this;
            },
            weekOfMonth: function() {
                add('wm', 1, applyMax ? 0 : 5);
                return this;
            },
            weekOfYear: function() {
                add('wy', 1, applyMax ? 0 : 53);
                return this;
            },
            month: function() {
                add('M', 1, 12);
                return this;
            },
            year: function() {
                add('Y', 1970, 2450);
                return this;
            },
            fullDate: function() {
                for (var i = 0, len = values.length; i < len; i++) {
                    values[i] = values[i].getTime();
                }
                add('fd');
                return this;
            },
            customModifier: function(id) {
                var custom = later.modifier[id];
                if (!custom)
                    throw new Error(
                        'Custom modifier ' + id + ' not recognized!'
                    );
                modifier = id;
                values =
                    arguments[1] instanceof Array
                        ? arguments[1]
                        : [arguments[1]];
                return this;
            },
            customPeriod: function(id) {
                var custom = later[id];
                if (!custom)
                    throw new Error(
                        'Custom time period ' + id + ' not recognized!'
                    );
                add(
                    id,
                    custom.extent(new Date())[0],
                    custom.extent(new Date())[1]
                );
                return this;
            },
            startingOn: function(start) {
                return this.between(start, last.m);
            },
            between: function(start, end) {
                cur[last.n] = cur[last.n].splice(0, last.c);
                every = last.x;
                add(last.n, start, end);
                return this;
            },
            and: function() {
                cur = curArr[curArr.push({}) - 1];
                return this;
            },
            except: function() {
                curArr = exceptions;
                cur = null;
                return this;
            }
        };
    };
    later.parse.text = function(str) {
        var recur = later.parse.recur,
            pos = 0,
            input = '',
            error;
        var TOKENTYPES = {
            eof: /^$/,
            rank: /^((\d+)(st|nd|rd|th)?)\b/,
            time: /^((([0]?[1-9]|1[0-2]):[0-5]\d(\s)?(am|pm))|(([0]?\d|1\d|2[0-3]):[0-5]\d))\b/,
            dayName: /^((sun|mon|tue(s)?|wed(nes)?|thu(r(s)?)?|fri|sat(ur)?)(day)?)\b/,
            monthName: /^(jan(uary)?|feb(ruary)?|ma((r(ch)?)?|y)|apr(il)?|ju(ly|ne)|aug(ust)?|oct(ober)?|(sept|nov|dec)(ember)?)\b/,
            yearIndex: /^(\d\d\d\d)\b/,
            every: /^every\b/,
            after: /^after\b/,
            before: /^before\b/,
            second: /^(s|sec(ond)?(s)?)\b/,
            minute: /^(m|min(ute)?(s)?)\b/,
            hour: /^(h|hour(s)?)\b/,
            day: /^(day(s)?( of the month)?)\b/,
            dayInstance: /^day instance\b/,
            dayOfWeek: /^day(s)? of the week\b/,
            dayOfYear: /^day(s)? of the year\b/,
            weekOfYear: /^week(s)?( of the year)?\b/,
            weekOfMonth: /^week(s)? of the month\b/,
            weekday: /^weekday\b/,
            weekend: /^weekend\b/,
            month: /^month(s)?\b/,
            year: /^year(s)?\b/,
            between: /^between (the)?\b/,
            start: /^(start(ing)? (at|on( the)?)?)\b/,
            at: /^(at|@)\b/,
            and: /^(,|and\b)/,
            except: /^(except\b)/,
            also: /(also)\b/,
            first: /^(first)\b/,
            last: /^last\b/,
            in: /^in\b/,
            of: /^of\b/,
            onthe: /^on the\b/,
            on: /^on\b/,
            through: /(-|^(to|through)\b)/
        };
        var NAMES = {
            jan: 1,
            feb: 2,
            mar: 3,
            apr: 4,
            may: 5,
            jun: 6,
            jul: 7,
            aug: 8,
            sep: 9,
            oct: 10,
            nov: 11,
            dec: 12,
            sun: 1,
            mon: 2,
            tue: 3,
            wed: 4,
            thu: 5,
            fri: 6,
            sat: 7,
            '1st': 1,
            fir: 1,
            '2nd': 2,
            sec: 2,
            '3rd': 3,
            thi: 3,
            '4th': 4,
            for: 4
        };

        function t(start, end, text, type) {
            return {
                startPos: start,
                endPos: end,
                text: text,
                type: type
            };
        }

        function peek(expected) {
            var scanTokens = expected instanceof Array ? expected : [expected],
                whiteSpace = /\s+/,
                token,
                curInput,
                m,
                scanToken,
                start,
                len;
            scanTokens.push(whiteSpace);
            start = pos;
            while (!token || token.type === whiteSpace) {
                len = -1;
                curInput = input.substring(start);
                token = t(start, start, input.split(whiteSpace)[0]);
                var i,
                    length = scanTokens.length;
                for (i = 0; i < length; i++) {
                    scanToken = scanTokens[i];
                    m = scanToken.exec(curInput);
                    if (m && m.index === 0 && m[0].length > len) {
                        len = m[0].length;
                        token = t(
                            start,
                            start + len,
                            curInput.substring(0, len),
                            scanToken
                        );
                    }
                }
                if (token.type === whiteSpace) {
                    start = token.endPos;
                }
            }
            return token;
        }

        function scan(expectedToken) {
            var token = peek(expectedToken);
            pos = token.endPos;
            return token;
        }

        function parseThroughExpr(tokenType) {
            var start = +parseTokenValue(tokenType),
                end = checkAndParse(TOKENTYPES.through)
                    ? +parseTokenValue(tokenType)
                    : start,
                nums = [];
            for (var i = start; i <= end; i++) {
                nums.push(i);
            }
            return nums;
        }

        function parseRanges(tokenType) {
            var nums = parseThroughExpr(tokenType);
            while (checkAndParse(TOKENTYPES.and)) {
                nums = nums.concat(parseThroughExpr(tokenType));
            }
            return nums;
        }

        function parseEvery(r) {
            var num, period, start, end;
            if (checkAndParse(TOKENTYPES.weekend)) {
                r.on(NAMES.sun, NAMES.sat).dayOfWeek();
            } else if (checkAndParse(TOKENTYPES.weekday)) {
                r.on(
                    NAMES.mon,
                    NAMES.tue,
                    NAMES.wed,
                    NAMES.thu,
                    NAMES.fri
                ).dayOfWeek();
            } else {
                num = parseTokenValue(TOKENTYPES.rank);
                r.every(num);
                period = parseTimePeriod(r);
                if (checkAndParse(TOKENTYPES.start)) {
                    num = parseTokenValue(TOKENTYPES.rank);
                    r.startingOn(num);
                    parseToken(period.type);
                } else if (checkAndParse(TOKENTYPES.between)) {
                    start = parseTokenValue(TOKENTYPES.rank);
                    if (checkAndParse(TOKENTYPES.and)) {
                        end = parseTokenValue(TOKENTYPES.rank);
                        r.between(start, end);
                    }
                }
            }
        }

        function parseOnThe(r) {
            if (checkAndParse(TOKENTYPES.first)) {
                r.first();
            } else if (checkAndParse(TOKENTYPES.last)) {
                r.last();
            } else {
                r.on(parseRanges(TOKENTYPES.rank));
            }
            parseTimePeriod(r);
        }

        function parseScheduleExpr(str) {
            pos = 0;
            input = str;
            error = -1;
            var r = recur();
            while (pos < input.length && error < 0) {
                var token = parseToken([
                    TOKENTYPES.every,
                    TOKENTYPES.after,
                    TOKENTYPES.before,
                    TOKENTYPES.onthe,
                    TOKENTYPES.on,
                    TOKENTYPES.of,
                    TOKENTYPES['in'],
                    TOKENTYPES.at,
                    TOKENTYPES.and,
                    TOKENTYPES.except,
                    TOKENTYPES.also
                ]);
                switch (token.type) {
                    case TOKENTYPES.every:
                        parseEvery(r);
                        break;

                    case TOKENTYPES.after:
                        if (peek(TOKENTYPES.time).type !== undefined) {
                            r.after(parseTokenValue(TOKENTYPES.time));
                            r.time();
                        } else {
                            r.after(parseTokenValue(TOKENTYPES.rank));
                            parseTimePeriod(r);
                        }
                        break;

                    case TOKENTYPES.before:
                        if (peek(TOKENTYPES.time).type !== undefined) {
                            r.before(parseTokenValue(TOKENTYPES.time));
                            r.time();
                        } else {
                            r.before(parseTokenValue(TOKENTYPES.rank));
                            parseTimePeriod(r);
                        }
                        break;

                    case TOKENTYPES.onthe:
                        parseOnThe(r);
                        break;

                    case TOKENTYPES.on:
                        r.on(parseRanges(TOKENTYPES.dayName)).dayOfWeek();
                        break;

                    case TOKENTYPES.of:
                        r.on(parseRanges(TOKENTYPES.monthName)).month();
                        break;

                    case TOKENTYPES['in']:
                        r.on(parseRanges(TOKENTYPES.yearIndex)).year();
                        break;

                    case TOKENTYPES.at:
                        r.on(parseTokenValue(TOKENTYPES.time)).time();
                        while (checkAndParse(TOKENTYPES.and)) {
                            r.on(parseTokenValue(TOKENTYPES.time)).time();
                        }
                        break;

                    case TOKENTYPES.and:
                        break;

                    case TOKENTYPES.also:
                        r.and();
                        break;

                    case TOKENTYPES.except:
                        r.except();
                        break;

                    default:
                        error = pos;
                }
            }
            return {
                schedules: r.schedules,
                exceptions: r.exceptions,
                error: error
            };
        }

        function parseTimePeriod(r) {
            var timePeriod = parseToken([
                TOKENTYPES.second,
                TOKENTYPES.minute,
                TOKENTYPES.hour,
                TOKENTYPES.dayOfYear,
                TOKENTYPES.dayOfWeek,
                TOKENTYPES.dayInstance,
                TOKENTYPES.day,
                TOKENTYPES.month,
                TOKENTYPES.year,
                TOKENTYPES.weekOfMonth,
                TOKENTYPES.weekOfYear
            ]);
            switch (timePeriod.type) {
                case TOKENTYPES.second:
                    r.second();
                    break;

                case TOKENTYPES.minute:
                    r.minute();
                    break;

                case TOKENTYPES.hour:
                    r.hour();
                    break;

                case TOKENTYPES.dayOfYear:
                    r.dayOfYear();
                    break;

                case TOKENTYPES.dayOfWeek:
                    r.dayOfWeek();
                    break;

                case TOKENTYPES.dayInstance:
                    r.dayOfWeekCount();
                    break;

                case TOKENTYPES.day:
                    r.dayOfMonth();
                    break;

                case TOKENTYPES.weekOfMonth:
                    r.weekOfMonth();
                    break;

                case TOKENTYPES.weekOfYear:
                    r.weekOfYear();
                    break;

                case TOKENTYPES.month:
                    r.month();
                    break;

                case TOKENTYPES.year:
                    r.year();
                    break;

                default:
                    error = pos;
            }
            return timePeriod;
        }

        function checkAndParse(tokenType) {
            var found = peek(tokenType).type === tokenType;
            if (found) {
                scan(tokenType);
            }
            return found;
        }

        function parseToken(tokenType) {
            var t = scan(tokenType);
            if (t.type) {
                t.text = convertString(t.text, tokenType);
            } else {
                error = pos;
            }
            return t;
        }

        function parseTokenValue(tokenType) {
            return parseToken(tokenType).text;
        }

        function convertString(str, tokenType) {
            var output = str;
            switch (tokenType) {
                case TOKENTYPES.time:
                    var parts = str.split(/(:|am|pm)/),
                        hour =
                            parts[3] === 'pm' && parts[0] < 12
                                ? parseInt(parts[0], 10) + 12
                                : parts[0],
                        min = parts[2].trim();
                    output = (hour.length === 1 ? '0' : '') + hour + ':' + min;
                    break;

                case TOKENTYPES.rank:
                    output = parseInt(/^\d+/.exec(str)[0], 10);
                    break;

                case TOKENTYPES.monthName:
                case TOKENTYPES.dayName:
                    output = NAMES[str.substring(0, 3)];
                    break;
            }
            return output;
        }
        return parseScheduleExpr(str.toLowerCase());
    };
    return later;
})();

export default later;
